# -*- coding: UTF-8 -*-
#    Gedit OpenMSX plugin
#    Copyright (C) 2006-2008  Rafael Jannone <rafael AT jannone DOT org>
#
#    This program is free software; you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation; either version 2 of the License, or
#    (at your option) any later version.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    You should have received a copy of the GNU General Public License
#    along with this program; if not, write to the Free Software
#    Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA

#version 0.3.0

import gedit
import gtk
import re
import os
import popen2
import gettext
import time
import ConfigParser
from gtk import glade
from xml.sax import saxutils
from stat import *
from outputpanel import OutputPanel
from wrdsk import WrDSK, DirEntry

# spawn and control the flow of messages to openmsx
# it was intended to be a thread, but threading don't work too well inside gedit
class OpenMSXLauncher:
	def __init__(self):
		self.disk = os.tempnam()
		self.pipe = None
		self.cmdLine = 'openmsx'
		
	def __destroy__(self):
		os.path.unlink(self.disk)
		try:
			print >>self.pipe, "</openmsx-control>"
			self.pipe.flush()
		except:
			pass	

	def openEmulator(self):
		#print "openning emulator"
		self.pipe = os.popen(self.cmdLine + ' -control stdio:', 'w')			
		print >>self.pipe, "<openmsx-control>\n"
		self.pipe.flush()
		self.command('diska ' + self.disk)
		self.sendCommand('set power on')
		self.sendCommand('restoredefault renderer')			
		self.sendCommand('set throttle off')
		self.sendCommand('set fullspeedwhenloading on')
		#time.sleep(2)
		self.sendCommand('after time 20 { set throttle on ; set fullspeedwhenloading on }');

	def sendCommand(self, txt):
		line = "<command>" + saxutils.escape(txt) + "</command>\n"
		#print "sending: " + line
		print >>self.pipe, line
		self.pipe.flush()

	def command(self, txt):
		self.sendCommand(txt)

	def start(self):
		self.run()

	def isAlive(self):
		return self.pipe and not self.pipe.closed

	def run(self):
		try:
			self.openEmulator()
		except:
			print "Initialization aborted"
			self.pipe=None

# our plugin class
class OpenMSXPlugin(gedit.Plugin):	

	GLADE_FILE = os.path.join(os.path.dirname(__file__), "glade/openmsx.glade")
	CONFIG_FILE = os.path.join(os.path.dirname(__file__), "config.ini")
	
	def __init__(self):
		gedit.Plugin.__init__(self)
		self.controls = {}
		self.autoType = []
		self.disk = None
		self.dialog = None
		self.runScript = None
		self.output = None
		self.configured = False
		self.initConfig()
		
	def __destroy__(self):
		return
		# i am not sure wether it's an advantage to close all openmsx instances upon exit
		for control in self.controls.values():
			try:
				control.command('quit')
				control.join()
			except:
				pass
				
	def initConfig(self):
		config = ConfigParser.SafeConfigParser()
		config.add_section("openmsx")
		config.set("openmsx", "command_line", "openmsx")
		config.set("openmsx", "disk_path", "")
		config.set("openmsx", "disk_path_from_doc", str(True))
		config.set("openmsx", "auto_type", "")
		config.set("openmsx", "run_script", str(False))
		config.set("openmsx", "run_script_line", "make")		
		self.config = config
		
	def loadConfig(self):
		config = self.config
		try:
			config.readfp(open(self.CONFIG_FILE))
		except:
			pass
		self.cmdLine.set_text(config.get("openmsx", "command_line"))
		self.textDiskPath.set_text(config.get("openmsx", "disk_path"))
		self.checkUseDocPath.set_active(config.getboolean("openmsx", "disk_path_from_doc"))
		buffer = self.areaAutoType.get_buffer()		
		buffer.set_text(config.get("openmsx", "auto_type"))
		self.checkUseScript.set_active(config.getboolean("openmsx", "run_script"))
		self.textScript.set_text(config.get("openmsx", "run_script_line"))

	def saveConfig(self):
		config = self.config
		config.set("openmsx", "command_line", self.cmdLine.get_text())
		config.set("openmsx", "disk_path", self.textDiskPath.get_text())
		config.set("openmsx", "disk_path_from_doc", str(self.checkUseDocPath.get_active()))
		buffer = self.areaAutoType.get_buffer()
		start, end = buffer.get_bounds()				
		config.set("openmsx", "auto_type", buffer.get_text(start, end))
		config.set("openmsx", "run_script", str(self.checkUseScript.get_active()))
		config.set("openmsx", "run_script_line", self.textScript.get_text())
		config.write(open(self.CONFIG_FILE, "w"))
		print "Config saved"
				
	def alert(self, par, typ, msg):
		d = gtk.MessageDialog(par, gtk.DIALOG_MODAL, typ, gtk.BUTTONS_OK, msg)
		d.run()
		d.destroy()

	def getDisk(self):
		disk = self.window.get_active_document().get_uri()
		if disk:
			handler = disk[0:7]
			if handler == 'file://':
				disk = os.path.dirname(disk[7:])
			else:
				disk = None
		return disk
		
	def getFilename(self):
		disk = self.window.get_active_document().get_uri()
		if disk:
			handler = disk[0:7]
			if handler == 'file://':
				disk = disk[7:]
			else:
				disk = None
		return disk	

	def fillDefaultDialog(self):
		disk = self.getDisk()
		if disk:
			self.textDiskPath.set_text(disk)

	def showConfigDialog(self):
		self.configured = True
		if self.dialog:
			self.dialog.show()
			return
	
		callbacks = {
			'on_btnOK_clicked' : self.onBtnOKClicked,
			'on_btnRun_clicked' : self.onBtnRunClicked,			
			'on_useDocPath_toggled' : self.onUseDocPathToggled,
			'on_useScript_toggled' : self.onUseScriptToggled,
			'on_btnFolder_clicked' : self.onBtnFolderClicked,
			'on_btnScript_clicked' : self.onBtnScriptClicked
		}

		#glade.set_custom_handler(self.custom_handler)
		self.ui = glade.XML(self.GLADE_FILE, 'configDialog')
		self.ui.signal_autoconnect(callbacks)
		self.cmdLine = self.ui.get_widget('cmdLine')
		self.dialog = self.ui.get_widget('configDialog')
		self.textDiskPath = self.ui.get_widget('diskPath')
		self.textScript = self.ui.get_widget('script')		
		self.checkUseDocPath = self.ui.get_widget('useDocPath')
		self.checkUseScript = self.ui.get_widget('useScript')		
		self.areaAutoType = self.ui.get_widget('autoType')
		self.btnFolder = self.ui.get_widget('btnFolder')
		self.btnScript = self.ui.get_widget('btnScript')
		self.fillDefaultDialog()
		# prevent window destruction when hitting the close ("X") button
		self.dialog.connect("delete_event", self.onDeleteWindow)
		self.loadConfig()
		self.dialog.show()

	def onDeleteWindow(self, *args):
		self.dialog.hide()
		self.saveConfig()
		return True

	def onUseScriptToggled(self, check):
		enabled = check.get_active()
		self.textScript.set_sensitive(enabled)	
		self.btnScript.set_sensitive(enabled)

	def onUseDocPathToggled(self, check):
		enabled = not check.get_active()
		self.textDiskPath.set_sensitive(enabled)
		self.btnFolder.set_sensitive(enabled)

	def onRun(self, item):
		if not self.configured:
			return self.showConfigDialog()
	
		# prepare disk path
		if self.checkUseDocPath.get_active():
			path = self.getDisk()
		else:
			path = self.textDiskPath.get_text()
		self.disk = path
		
		# prepare autotyping lines		
		buffer = self.areaAutoType.get_buffer()
		start, end = buffer.get_bounds()		
		self.autoType = buffer.get_text(start, end).split("\n")
		
		# prepare script		
		self.runScript = None
		if self.checkUseScript.get_active():
			self.runScript = self.textScript.get_text()
			if self.runScript == '':
				self.runScript = None
				
		# go go gekinganger! ^^v
		self.launch()

	def onBtnRunClicked(self, button):
		self.saveConfig()
		self.dialog.hide()
		self.onRun(None)

	def onBtnOKClicked(self, button):
		self.saveConfig()
		self.dialog.hide()

	def onBtnFolderClicked(self, button):
		#gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER
		chooser = gtk.FileChooserDialog(title=None,action=gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER,
			buttons=(gtk.STOCK_CANCEL,gtk.RESPONSE_CANCEL,gtk.STOCK_OPEN,gtk.RESPONSE_OK))
		response = chooser.run()
		if response == gtk.RESPONSE_OK:
			self.textDiskPath.set_text(chooser.get_filename())
		#elif response == gtk.RESPONSE_CANCEL:
		chooser.destroy()			

	def onBtnScriptClicked(self, button):
		#gtk.FILE_CHOOSER_ACTION_OPEN
		chooser = gtk.FileChooserDialog(title=None,action=gtk.FILE_CHOOSER_ACTION_OPEN,
			buttons=(gtk.STOCK_CANCEL,gtk.RESPONSE_CANCEL,gtk.STOCK_OPEN,gtk.RESPONSE_OK))
		response = chooser.run()
		if response == gtk.RESPONSE_OK:
			self.textScript.set_text(chooser.get_filename())
		#elif response == gtk.RESPONSE_CANCEL:
		chooser.destroy()			

	def onConfigure(self, item):	
		self.showConfigDialog()
	
	def launch(self):
		disk = self.disk
		fname = self.getFilename()		
		if disk == None or fname == None:
			self.alert(None, gtk.MESSAGE_ERROR, "You must save the file first")
			return					
		fpath = os.path.abspath(os.path.dirname(fname))
		chdir = "cd " + fpath.encode('string-escape') + ";"
		
		# run script, when available
		if self.runScript:			
			if not self.output:
				self.output = OutputPanel(self.window)
				bottom = self.window.get_bottom_panel()
				bottom.add_item(self.output.panel, "Shell Output", gtk.STOCK_EXECUTE)
			self.output.clear()
			self.output.show()
			self.output.write(">> Running " + self.runScript + " ...\n")
			sc = popen2.Popen4(chdir + self.runScript)				
			sc.tochild.close()
			result = sc.wait()				
			for line in sc.fromchild:
				self.output.write(line)
			sc.fromchild.close()				
			if result != None and result != 0:
				self.output.write("\n>> Process terminated with error status " + str(result))
				return

		# spawn a control as needed
		if not self.controls.has_key(disk):
			control = OpenMSXLauncher()
			self.controls[disk] = control
		else:
			control = self.controls[disk]
		
		control.cmdLine = chdir + self.cmdLine.get_text()
		
		# build disk
		fname = os.path.basename(fname)					
		files = os.listdir(disk)
		wrdsk = WrDSK(control.disk)
		for entryfile in files:
			mode = os.stat(disk + "/" + entryfile)[ST_MODE]
			if not S_ISREG(mode):
				continue
			dirpointer = wrdsk.findEmptyDir()
			if dirpointer == None:
				print "Disk is full"
				break
			entry = DirEntry(disk + "/" + entryfile)
			if entryfile != fname:
				textEncoding = entry.ext in ["BAS", "ASM", "S  ", "C  ", "PAS", "TXT", "IDZ"]
				if entry.ext == "BAS":
					preview = open(entry.path, "rb")
					first = preview.read(1)
					if first == chr(255):
						textEncoding = False
					preview.close()
				if textEncoding:
					txt = open(entry.path, "r").read()
					txt = txt.replace('\r', '').replace('\n', '\r\n')
					entry.createFromString(wrdsk, txt)
				else:
					entry.create(wrdsk)
			else:
				doc = self.window.get_active_document()
				start, end = doc.get_bounds()
				txt = doc.get_text(start, end).replace('\r', '').replace('\n', '\r\n')
				entry.createFromString(wrdsk, txt)				
			wrdsk.writeEntry(entry, dirpointer)
		wrdsk.close()
		
		# test if running
		try:
			control.command('set fullspeedwhenloading on')
			control.command('set throttle off')
			control.command('diska ' + control.disk)
			control.command('reset')
			#time.sleep(2)
			control.command('after time 20 { set throttle on ; set fullspeedwhenloading on }');
		except:
			control.start()
			
		if control.pipe == None:
			self.alert(None, gtk.MESSAGE_ERROR, "OpenMSX could not be launched. Check your configuration.")
			return
			
		# we set throttle off for maximum speed, even when autotyping
		# only after that we restore normal speed
		for txt in self.autoType:
			if txt != '':
				txt = txt.replace('%s', fname)
				if txt[0] == '/':
					txt = txt[1:]
					if txt[:5] == 'sleep':
						amount = float(txt[5:])
						time.sleep(amount)
					else:
						control.command(txt)
				else:
					#txt = txt.replace('"', '@')
					control.command('type "' + re.escape(txt) + '\\r"')
		# restore normal speed
		#control.command('set throttle on')

	def insertMenu(self):
		#print "inserting menu"
		manager = self.window.get_ui_manager()

		self.action_group = gtk.ActionGroup("GeditOpenMSXPluginActions")
		self.action_group.set_translation_domain('gedit')
		self.action_group.add_actions([('ConfigOpenMSX', None,
				_('_Configure OpenMSX...'), \
				'<Ctrl>F5', _('Configure OpenMSX launching options'), \
				self.onConfigure)])
		self.action_group.add_actions([('RunOpenMSX', None,
				_('_Run OpenMSX'), \
				'F5', _('Launch/Reload the disk'), \
				self.onRun)])

		self.merge_id = manager.new_merge_id()
		manager.insert_action_group(self.action_group, -1)		
		manager.add_ui(self.merge_id, '/MenuBar/ToolsMenu/ToolsOps_5', \
				'ConfigOpenMSX', 'ConfigOpenMSX', gtk.UI_MANAGER_MENUITEM, False)
		manager.add_ui(self.merge_id, '/MenuBar/ToolsMenu/ToolsOps_5', \
				'RunOpenMSX', 'RunOpenMSX', gtk.UI_MANAGER_MENUITEM, False)

	def removeMenu(self):
		#print "removing menu"
		manager = self.window.get_ui_manager()
		manager.remove_ui(self.merge_id)
		manager.remove_action_group(self.action_group)
		self.action_group = None
			
	def activate(self, window):
		self.window = window
		self.insertMenu()
	
	def deactivate(self, window):
		self.removeMenu()
		if self.output:
			bottom = self.window.get_bottom_panel()
			bottom.remove_item(self.output.panel)

	def update_ui(self, window):
		pass

